# 에러 핸들링

# try..catch 와 에러 핸들링

에러의 원인

  • 실수
  • 예기치 못한 사용자 입력
  • 잘못된 서버 응답 ....

에러 발생의 결과

  • 스크립트가 즉시 중단
  • 콘솔에 에러가 출력

try..catch

  • 스크립트가 죽는 것을 방지
  • 에러를 catch 해서 더 합당한 무언가를 할 수 있게 한다.

# 런타임 에러(예외 exception) 에서만 동작

  • 실행가능한 runnable 코드 : 유효한 자바스크립트 코드
  • 중괄호의 짝이 안맞는 것처럼 문법적으로 잘못된 경우 try..catch 가 동작하지 않는다.

# 동기적으로 동작한다.

setTimeout 처럼 비동기 함수에서 발생한 예외는 잡아낼 수 없다.

# 에러 객체

  1. 에러가 발생하면 자바스크립트는 에러 상세내용이 담긴 객체를 생성
  2. catch 블록에 에러 객체를 인수로 전달

# 에러 객체의 주요 프로퍼티

  1. name
    • 에러 이름
    • 'ReferenceError' : 정의되지 않은 변수 때문에 발생한 에러
  2. message
    • 에러 상세 내용을 담고 있는 문자 메세지
  3. stack
    • 널리 사용되는 비표준 프로퍼티
    • 현재 호출 스택, 에러를 유발한 중첩 호출들의 순서 정보를 가진 문자열
    • 디버깅 목적
try {
  lalala; // 에러, 변수가 정의되지 않음!
} catch(err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at ... (호출 스택)

  // 에러 전체를 보여줄 수도 있습니다.
  // 이때, 에러 객체는 "name: message" 형태의 문자열로 변환됩니다.
  alert(err); // ReferenceError: lalala is not defined
}
1
2
3
4
5
6
7
8
9
10
11

# 선택적 catch 바인딩

에러에 대한 자세한 정보가 필요하지 않을 때, catch 에서 인수를 생략

try {
  // ...
} catch { // <-- (err) 없이 쓸 수 있음
  // ...
}
1
2
3
4
5

구식 브라우저에는 폴리필이 필요

# try..catch 사용하기

JSON.parse

  • 인자에 잘못된 형식의 json 이 들어온 경우 JSON.parse 는 에러를 만든다.
const json = "{ bad json }";

try {

  let user = JSON.parse(json); // <-- 여기서 에러가 발생하므로
  alert( user.name ); // 이 코드는 동작하지 않습니다.

} catch (e) {
  // 에러가 발생하면 제어 흐름이 catch 문으로 넘어옵니다.
  alert( "데이터에 에러가 있어 재요청을 시도합니다." );
  alert( e.name );
  alert( e.message );
}
1
2
3
4
5
6
7
8
9
10
11
12
13

catch 블록에서 할 수 있는 일

  • 새로운 네트워크 요청 보내기
  • 사용자에게 대안 제안하기
  • 로깅 장치에 에러 정보 보내기

# 직접 에러를 만들어서 던지기

# throw 연산자

에러를 생성한다. throw <error object>

  • 에러 객체

    • 숫자, 문자열 같은 원시형 자료를 포함한 무엇이든 가능
    • 내장 에러와의 호환을 위해 : { name, message } 프로퍼티 권장
  • 표준 에러 객체 관련 생성자

    • Error, SyntaxError, ReferenceError, TypeError
    let error = new Error(message);
      error = new SyntaxError(message);
      error = new ReferenceError(message);
    
      error = new Error("이상한 일이 발생했습니다. o_O");  
    alert(error.name); // Error
    alert(error.message); // 이상한 일이 발생했습니다. o_O
    
    1
    2
    3
    4
    5
    6
    7

name 프로퍼티가 없으면 예외 처리

const json = '{ "age": 30 }'; // 불완전한 데이터

try {

  let user = JSON.parse(json); // <-- 에러 없음
  alert( user.name ); // 이름이 없습니다!

} catch (e) {
  alert( "실행되지 않습니다." );
}
1
2
3
4
5
6
7
8
9
10
const json = '{ "age": 30 }'; // 불완전한 데이터

try {

  const user = JSON.parse(json); // <-- 에러 없음
  if (!user.name) 
    throw new SyntaxError("불완전한 데이터: 이름 없음"); // (*)
  alert( user.name );

} catch(e) {
  alert( "JSON Error: " + e.message ); // JSON Error: 불완전한 데이터: 이름 없음
}
1
2
3
4
5
6
7
8
9
10
11
12

# 에러 다시 던지기

목표: user 앞에 let 을 붙이지 않아 발생하는 에러를 처리

문제: Json SyntaxError 만 처리해주는 alert 만 존재한다.

'use strict';

const json = '{ "age": 30 }'; // 불완전한 데이터

try {
  user = JSON.parse(json); // <-- user 앞에 let을 붙이는 걸 잊었네요.
  // ...
} catch(err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (실제론 JSON Error가 아닙니다.)
}
1
2
3
4
5
6
7
8
9
10
11

user 앞에 let 을 붙이지 않아 발생하는 ReferenceError 는 외부에서 처리하도록 catch 블록에서 throw 를 한번 더 해준다.

const json = '{ "age": 30 }'; // 불완전한 데이터
try {

  user = JSON.parse(json);
  if (!user.name)
    throw new SyntaxError("불완전한 데이터: 이름 없음");
  blabla(); // 예상치 못한 에러
  alert( user.name );

} catch(e) {

  if (e instanceof SyntaxError) {
    alert( "JSON Error: " + e.message );
  } else {
    throw e; // 에러 다시 던지기 (*)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

catch 안의 throw e

  1. try..catch 밖의 try..catch 문에 걸리거나
function readData() {
  const json = '{ "age": 30 }';
  try {
    // ...
    blabla(); // 에러!
  } catch (e) {
    // ...
    if (!(e instanceof SyntaxError)) {
      throw e; // 알 수 없는 에러 다시 던지기
    }
  }
}

try {
  readData();
} catch (e) {
  alert( "External catch got: " + e ); // 에러를 잡음
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 스크립트가 죽음

# try..catch..finally

function x() {
    try {
       // ... 코드를 실행 ...
        return;
    } catch(e) {
       //... 에러 핸들링 ...
    } finally {
        // ... 항상 실행 ...
        // 작업을 초기화 하는 코드
        console.log('execution')
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
function x() {
    try {
       // ... 코드를 실행 ...
    } catch(e) {
       //... 에러 핸들링 ...
    } 
    console.log('execution')
}
1
2
3
4
5
6
7
8

두 예제의 차이점

  • try 에서 return 이 발생하면 두 번째 예제의 execution 은 출력되지 않는다.

# try..finally

  • 안에서 에러를 처리하고 싶지 않을 때
  • 시작한 프로세스가 마무리 되었는지 확실히 하고 싶은 경우
function func() {
  // 무언가를 측정하는 경우와 같이 끝맺음이 있어야 하는 프로세스
  try {
    // ...
  } finally {
    // 스크립트가 죽더라도 완료됨
  }
}
1
2
3
4
5
6
7
8
  • try 안에서 발생한 에러는 외부에서 catch 해주어야 한다.
  • finally 는 실행흐름이 함수를 떠나기 전에 실행된다.

# 전역 catch

호스트 환경에 따라 다르다.

node.js process.on('uncaughtException')

브라우저 window.onerror

<script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n At ${line}:${col} of ${url}`);
  // message 에러 메시지
  // url 에러가 발생한 스크립트의 URL
  // line, col 에러가 발생한 곳의 줄과 열 번호
  // error 에러 객체
  };

  function readData() {
    badFunc(); // 에러가 발생한 장소
  }
  readData();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 죽어버린 스크립트를 복구하려는 목적보다 개발자에게 에러 메시지를 보내는 용도로 사용한다.

# 커스텀 에러와 확장

Error 를 상속받아 커스텀에러를 만드는 것의 장점

  • obj instanceof Error 를 사용하여 에러 식별 가능
  • 등 Error 객체의 장점들을 사용할 수 있다.

# 에러 확장하기

자바스크립트 자체 내장 에러 클래스 Error 의 '슈도 코드'

class Error {
  constructor(message) {
    this.message = message;
    this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
    this.stack = <call stack>;  // stack은 표준은 아니지만, 대다수 환경이 지원합니다.
  }
}
1
2
3
4
5
6
7

Error 를 상속받은 ValidationError

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}
function test() {
    throw new ValidationError('에러 발생!');
}

try {
    test();
} catch (err) {
    alert(err.message); // 에러 발생!
    alert(err.name); // ValidationError
    alert(err.stack); // 각 행 번호가 있는 중첩된 호출들의 목록
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

readUser(json) 라는 함수를 만들어보자

  • SyntaxError : JSON 형식인지 검사
  • ValidationError: 유효한 유저 데이터인지 검사
function readUser(json) {
  let user = JSON.parse(json);

  if (!user.age) {
    throw new ValidationError("No field: age");
  }
  if (!user.name) {
    throw new ValidationError("No field: name");
  }

  return user;
}

try {
  let user = readUser('{ "age": 25 }');
} catch (err) {
  if (err instanceof ValidationError) {
    alert("Invalid data: " + err.message); // Invalid data: No field: name
  } else if (err instanceof SyntaxError) { // (*)
    alert("JSON Syntax Error: " + err.message);
  } else {
    throw err; // 알려지지 않은 에러는 재던지기 합니다. (**)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 더 깊게 상속하기

Validation Error 에서 프로퍼티를 확장하여 PropertyRequiredError 를 만들어 보자

  • 필수 프로퍼티가 없는 경우 예외 처리
class PropertyRequiredError extends ValidationError {
    constructor(property) {
        super("No property: " + property);
        this.name = "PropertyRequiredError";
        this.property = property;
    }
}

function readUser(json) {
    let user = JSON.parse(json);
    if (!user.age) 
        throw new PropertyRequiredError('age');
    if (!user.name) 
        throw new PropertyRequiredError('name');
    return user;
}

try {
  let user = readUser('{ "age": 25 }');
} catch (err) {
  if (err instanceof ValidationError) {
    alert("Invalid data: " + err.message); // Invalid data: No property: name
    alert(err.name); // PropertyRequiredError
    alert(err.property); // name
  } else if (err instanceof SyntaxError) {
    alert("JSON Syntax Error: " + err.message);
  } else {
    throw err; // 알려지지 않은 에러는 재던지기 합니다.
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

err.name == "SyntaxError" 보다 err instanceof SyntaxError 를 선호하는 이유

  • ValidationError 를 확장한 PropertyRequiredError 같은 새로운 확장 에러를 만들 때, instanceof 는 새로운 상속 클래스에서도 동작한다.

Error 객체의 name 자동할당

class MyError extends Error {
    constructor (message) {
        super(message);
        this.name = this.constructor.name;
    }
}
class ValidationError extends MyError { ... }
class PropertyRequiredError extends ValidationError {
  constructor(property) {
    super("No property: " + property);
    this.property = property;
  }
}
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 예외 감싸기

readUser 가 확장되면 커스텀 에러 클래스를 더 만들어야 한다.

try {
  //...
  readUser()  // 잠재적 에러 발생처
  // ...
} catch (err) {
  if (err instanceof ValidationError) {
    // validation 에러 처리
  } else if (err instanceof SyntaxError) {
    // 문법 에러 처리
  } else {
    throw err; // 알 수 없는 에러는 다시 던지기 함
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

하지만 실제로 필요로 하는 정보는 '데이터를 읽을 때' 에러 발생 여부이다.

예외 감싸기

  • 널리 알려진 예외처리 기술
  • 모든 에러를 종류별로 처리 하지 않는다.
  • 모든 에러를 포함할 수 있는 추상 에러를 하나 만든다.
  • 에러가 발생하면 추상 에러를 던지도록 한다.

에러 발생 여부를 가려내기 위해 예외 감싸기 (wrapping exception) 을 해보자

예외 감싸기의 장점

  • readUser 를 호출하는 코드에선 ReadError 만 확인
  • 추가 정보가 필요한 경우엔 cause 프로퍼티를 확인
  1. ReadError 데이터 읽기와 같은 포괄적인 에러를 대변하는 클래스
    class ReadError extends Error {
      constructor(message, cause) {
        super(message);
        this.cause = cause;
        this.name = 'ReadError';
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
  2. ValidationError, SyntaxError 는 readUser 내부에서 ReadError 를 생성함
    class ValidationError extends Error { /*...*/ }
    class PropertyRequiredError extends ValidationError { /* ... */ }
    
    function validateUser(user) {
      if (!user.age) {
        throw new PropertyRequiredError("age");
      }
    
      if (!user.name) {
        throw new PropertyRequiredError("name");
      }
    }
    
    function readUser(json) {
      let user;
    
      try {
        user = JSON.parse(json);
      } catch (err) {
        if (err instanceof SyntaxError) {
          throw new ReadError("Syntax Error", err);
        } else {
          throw err;
        }
      }
    
      try {
        validateUser(user);
      } catch (err) {
        if (err instanceof ValidationError) {
          throw new ReadError("Validation Error", err);
        } else {
          throw err;
        }
      }
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
  3. ReadError 객체의 cause 프로퍼티엔 실제 에러에 대한 참조 저장
    try {
      readUser('{잘못된 형식의 json}');
    } catch (e) {
      if (e instanceof ReadError) {
        alert(e);
        // Original error: SyntaxError: Unexpected token b in JSON at position 1
        alert("Original error: " + e.cause);
      } else {
        throw e;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

# Reference

  • https://ko.javascript.info/try-catch
  • https://ko.javascript.info/custom-errors